[org 0x7c00] ; 本程序代码加载到内存的位置是0x7c00

; 设置屏幕模式为文本模式，清除屏幕
mov ax, 3
int 0x10 ; BIOS系统调用，显示器相关功能
; 初始化段寄存器，如果不初始化的话一些虚拟机会报错，如vmware
mov ax, 0
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7c00 ; 初始化栈到0x7c00的位置

; xchg bx, bx ; 交换数据指令，bochs特有的魔数断点，测试用，在bochs的配置文件中的magic_break: enabled=1开启，就会在启动执行到这条指令时会产生断点，便于观察，

mov si, booting
call print


; jmp call_loder_err  ; 测试下加载loader的错误打印正不正常，测试后，这里注释，开发下面的加载loader的代码
; jmp $  ; 反复跳转到当前行

; 准备读硬盘参数
mov edi, 0x1000 ; 数据读入目标内存0x1000位置
mov ecx, 2 ; 在make file中，写入的是第二个扇区
mov bl, 4 ; 写入的扇区数量是4个
call read_disk
cmp word [0x1000], 0x55aa  ; 间接寻址0x1000读入的位置，看下是不是loader.asm代码中一开始写入的魔数0x55aa
jnz call_loder_err  ; jnz是指cmp比较不为0则跳转，也就是不为0x55aa时就跳转到错误打印，表示读入失败

; 如果正确的话，则跳转到代码中开始执行
jmp 0:0x1002  ; 表示跳转到代码段0，偏移0x1002的位置，因为魔数0x55aa占了两个字节，所以这里的0x1002才是Loader的正式的汇编代码的位置，也就是现在所写的打印字符串的地方。

; 阻塞在当前行，$表示当前行
jmp $  ; 反复跳转到当前行


read_disk:
    ; 1 设置读写扇区数量
    mov dx, 0x1f2 ; 设置读取扇区数量，0x1f2是设置读写扇区的数量寄存器
    mov al, bl ; 将bl设置好的参数1写入al
    out dx, al ; 只能用这两个寄存器，设置数量为1

    inc dx ; +1后为0x1f3
    mov al, cl ; 设置起始扇区的前八位，cl是ecx的低八位，存有扇区索引0
    out dx, al

    inc dx ; +1后为0x1f4
    shr ecx, 8 ; 向右移动8位，将中八位移动到低八位的位置
    mov al, cl ; 设置起始扇区的中八位
    out dx, al

    inc dx ; +1后为0x1f5
    shr ecx, 8 ; 向右移动8位，将中八位移动到低八位的位置
    mov al, cl ; 设置起始扇区的高八位
    out dx, al

    inc dx ; +1后为0x1f6
    shr ecx, 8 ; 将最后4位移动到cl的位置，用于设置起始扇区的24-27位
    and cl, 0b1111 ; 将高四位设置为0，最后只剩下0~3位
    mov al, 0b1110_0000 ; 5-7位固定为1，第4位为0表示主盘，第6位为1表示LBA模式
    or al, cl ; 将al和cl整合
    out dx, al

    inc dx ; +1后为0x1f7
    mov al, 0x20 ; 读硬盘
    out dx, al

    xor ecx, ecx ; 清空ecx，比mov ecx, 0的方式性能更高
    mov cl, bl ; 得到读写扇区的数量， cl是cx的低8位，cx是ecx的低16位
    
    .read: ; 读扇区代码块，每次读一个扇区
        push cx ; 因为reads函数内会修改cx，所以先保存好cx
        call .waits ; 等待数据准备完毕
        call .reads ; 读取一个扇区
        pop cx ; 恢复cx
        loop .read
    ret

    .waits:
        mov dx, 0x1f7 ; 硬盘状态寄存器，检查第3位是否准备完毕标识
        .check:
            in al, dx ; 读入到al
            jmp $+2 ; 直接跳转到下一行，相当于nop指令，但这样消耗的时钟周期更多
            jmp $+2 ; 延迟手段
            jmp $+2
            and al, 0b1000_1000 ; 只留下第3和第7位，假设硬盘不会出错，所以忽略第0位，其他位设置为0
            cmp al, 0b0000_1000 ; 看是否第3位为1，第7位为0，
            jnz .check ; 不为0（不相等）则调整回去继续读入+等待，一直check，相等则不跳转，终止循环
        ret

    .reads:
        mov dx, 0x1f0
        mov cx, 256 ; 一个扇区256字
        .readw:
            in ax, dx ; 读入一个字
            jmp $+2 ; 直接跳转到下一行，相当于nop指令，但这样消耗的时钟周期更多
            jmp $+2 ; 延迟手段
            jmp $+2
            mov [edi], ax ; 将ax数据写入内存
            add edi, 2 ; 将内存指针往后移动两个字节（一个字）
            loop .readw
        ret


call_loder_err:
    mov si, .err_msg
    call print
    hlt  ; cpu停止指令
    jmp $
    .err_msg:
        db "call loader program error!", 10, 13, 0

booting:
    db "booting on...", 10, 13, 0 ; 分别是 \n(换行) \r（将光标移动到气势位置） 0（字符串结束）

print:
    mov ah, 0x0e ; 设置的前置条件
    ; 创建一个循环打印
    .next:
        mov al, [si] ; 间接寻址，将si指向的字符放入al
        cmp al, 0 ; 和0比较，如果是0则表示字符串最后一个字符，循环结束
        jz .return ; jump if zero，当零标志位ZF为1时，执行跳转操作， cmp的两个操作数的值相等（相减为0），ZF标志位会被设置为1，则跳转到.return。
        ;jz指令的另一种写法是je指令，即jump if equal，它的作用与jz指令完全相同
        int 0x10  ; 如果不是最后一个0则要调用0x10中断bios的打印功能继续打印
        inc si ; 将si+1
        jmp .next
    .return:
    ret

; 主引导扇区共512字节，最后两个字节固定是0x55 0xaa，所以需要从上面代码结束位置开始到第510字节全部填充0
; 重复510-(当前行位置$-程序开始位置$$)次数，对应的字节全设置为0
times 510 - ($ - $$) db 0

; 设置两个字节的内容，主引导扇区的最后两个字节必须是0x55 0xaa
; dw 0xaadd  ; 也可以写成这样，小端存储
db 0x55, 0xaa

; 编译 nasm -f bin boot.asm -o boot.bin